Tailscale HTTPSのTLS証明書をTraefikで動的に取得する
ども、大瀧です。 Tailscale HTTPSはTailscaleのVPNノード間通信のためにLet's EncryptによるTLS証明書を提供するサービスです。これまではTailscale CLIとCaddyが証明書取得に対応していましたが、OSSのリバースプロキシTraefikでも利用できるようになっていたので試してみた様子を紹介します。
動作確認環境
- OS: Amazon Linux 2
- アーキテクチャ: Arm64
- Tailscale: v1.36.0
- Traefik: v3.0.0-beta2
Tailscale HTTPSとTraefikバージョン3はいずれもベータリリースの段階です。本番ワークロードには利用できないことに注意してください。手順は以下のブログ記事が詳しいです。
手順1. インストール
まずはTailscaleとTraefikをそれぞれインストールします(Traefikはワンバイナリなので、ダウンロードするだけです)。
$ curl -fsSL https://tailscale.com/install.sh | sh $ sudo tailscale up # 表示されるURLにブラウザからアクセスし、Tailscaleのアカウントで認証します $ wget https://github.com/traefik/traefik/releases/download/v3.0.0-beta2/traefik_v3.0.0-beta2_linux_arm64.tar.gz $ tar zxf traefik_v3.0.0-beta2_linux_arm64.tar.gz $ ls traefik traefik $
これでOKです。
手順2. 構成
Tailscale HTTPSはWeb管理画面の[DNS]で有効になっていることを確認します。
続いてTraefikです。今回はお試しなのでバイナリと同じカレントディレクトリにtraefik.toml
ファイルとdynamic.toml
ファイルを作成しました。
[entryPoints] [entryPoints.websecure] address = ":443" [providers] [providers.file] filename = "dynamic.toml" [certificatesResolvers.myresolver.tailscale] [api] debug = true [log] level = "DEBUG"
TraefikにはLet's EncryptのTLS証明書を取得するCertificate Resolversという仕組みがあり、ここにTailscale HTTPS(tailscale
)をセットします。9行目の部分です。
[http] [http.routers] [http.routers.towhoami] service = "whoami" rule = "Host(`ip-XX-XX-XX-XX.tailnet-XXXX.ts.net`)" [http.routers.towhoami.tls] certResolver = "myresolver" [http.services] [http.services.whoami] [http.services.whoami.loadBalancer] [[http.services.whoami.loadBalancer.servers]] # docker run -d -p 6060:80 traefik/whoami url = "http://localhost:6060"
2〜7行目のルーター設定には、TailscaleノードのDNS名ip-XX-XX-XX-XX.tailnet-XXXX.ts.net
へのアクセスに対して、先ほどのCertificate Resolversをセットしています。
9〜14行目のターゲット設定では`http://localhost:6060`にリクエストを転送するので、レスポンスを返すためのDockerコンテナを実行しておきます。
$ sudo amazon-linux-extras install docker $ sudo service docker start $ sudo docker run -d -p 6060:80 traefik/whoami $ curl localhost:6060 Hostname: ebdbc05b441b IP: 127.0.0.1 IP: 172.17.0.2 RemoteAddr: 172.17.0.1:48740 GET / HTTP/1.1 Host: localhost:6060 User-Agent: curl/7.79.1 Accept: */* $
これでOKです。
動作確認
ではTraefikを起動します。
$ sudo ./traefik 2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/cmd/traefik/traefik.go:100 > Traefik version 3.0.0-beta2 built on 2022-12-07T16:32:34Z version=3.0.0-beta2 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/cmd/traefik/traefik.go:107 > Static configuration loaded staticConfiguration={"api":{"dashboard":true,"debug":true},"certificatesResolvers":{"myresolver":{"tailscale":{}}},"entryPoints":{"websecure":{"address":":443","forwardedHeaders":{},"http":{},"http2":{"maxConcurrentStreams":250},"transport":{"lifeCycle":{"graceTimeOut":"10s"},"respondingTimeouts":{"idleTimeout":"3m0s"}},"udp":{"timeout":"3s"}}},"global":{"checkNewVersion":true},"log":{"format":"common","level":"DEBUG"},"providers":{"file":{"filename":"dynamic.toml","watch":true},"providersThrottleDuration":"2s"},"serversTransport":{"maxIdleConnsPerHost":200}} 2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/cmd/traefik/traefik.go:685 > Stats collection is disabled. Help us improve Traefik by turning this feature on :) More details on: https://doc.traefik.io/traefik/contributing/data-collection/ 2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:72 > Starting provider aggregator aggregator.ProviderAggregator 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/server_entrypoint_tcp.go:188 > Starting TCP Server entryPointName=websecure 2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *file.Provider 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *file.Provider provider configuration config={"filename":"dynamic.toml","watch":true} 2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *traefik.Provider 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *traefik.Provider provider configuration config={} 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:217 > Configuration received config={"http":{"serversTransports":{"default":{"maxIdleConnsPerHost":200}},"services":{"api":{},"dashboard":{},"noop":{}}},"tcp":{},"tls":{},"udp":{}} providerName=internal 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:217 > Configuration received config={"http":{"routers":{"towhoami":{"rule":"Host(`ip-XX-XX-XX-XX.tailnet-XXXX.ts.net`)","service":"whoami","tls":{"certResolver":"myresolver"}}},"services":{"whoami":{"loadBalancer":{"passHostHeader":true,"responseForwarding":{"flushInterval":"100ms"},"servers":[{"url":"http://localhost:6060"}]}}}},"tcp":{},"tls":{},"udp":{}} providerName=file 2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *acme.ChallengeTLSALPN 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *acme.ChallengeTLSALPN provider configuration config={} 2023-01-29T13:43:26Z INF github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:207 > Starting provider *tailscale.Provider 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/provider/aggregator/aggregator.go:208 > *tailscale.Provider provider configuration config={"ResolverName":"myresolver"} 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/tls/tlsmanager.go:294 > No default certificate, fallback to the internal generated certificate tlsStoreName=default 2023-01-29T13:43:26Z DBG github.com/traefik/traefik/v2/pkg/server/aggregator.go:47 > No entryPoint defined for this router, using the default one(s) instead entryPointName=["websecure"] routerName=towhoami 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/tls/tlsmanager.go:294 > No default certificate, fallback to the internal generated certificate tlsStoreName=default 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:256 > Creating load-balancer entryPointName=websecure routerName=towhoami@file serviceName=whoami@file 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:298 > Creating server entryPointName=websecure routerName=towhoami@file serverName=6d3e5017708777ea serviceName=whoami@file target=http://localhost:6060 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/tracing/forwarder.go:26 > Added outgoing tracing middleware entryPointName=websecure middlewareName=tracing middlewareType=TracingForwarder routerName=towhoami@file serviceName=whoami 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/recovery/recovery.go:22 > Creating middleware entryPointName=websecure middlewareName=traefik-internal-recovery middlewareType=Recovery 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/router/tcp/manager.go:235 > Adding route for ip-172-31-23-250.tailnet-96f8.ts.net with TLS options default entryPointName=websecure 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/provider/tailscale/provider.go:253 > Fetched certificate for domain "ip-172-31-23-250.tailnet-96f8.ts.net" providerName=myresolver.tailscale 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/configurationwatcher.go:217 > Configuration received config={"http":{},"tcp":{},"tls":{},"udp":{}} providerName=myresolver.tailscale 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/aggregator.go:47 > No entryPoint defined for this router, using the default one(s) instead entryPointName=["websecure"] routerName=towhoami 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/tls/certificate.go:158 > Adding certificate for domain(s) ip-XX-XX-XX-XX.tailnet-XXXX.ts.net 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/tls/tlsmanager.go:294 > No default certificate, fallback to the internal generated certificate tlsStoreName=default 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:256 > Creating load-balancer entryPointName=websecure routerName=towhoami@file serviceName=whoami@file 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/service/service.go:298 > Creating server entryPointName=websecure routerName=towhoami@file serverName=6d3e5017708777ea serviceName=whoami@file target=http://localhost:6060 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/tracing/forwarder.go:26 > Added outgoing tracing middleware entryPointName=websecure middlewareName=tracing middlewareType=TracingForwarder routerName=towhoami@file serviceName=whoami 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/middlewares/recovery/recovery.go:22 > Creating middleware entryPointName=websecure middlewareName=traefik-internal-recovery middlewareType=Recovery 2023-01-29T13:43:27Z DBG github.com/traefik/traefik/v2/pkg/server/router/tcp/manager.go:235 > Adding route for ip-XX-XX-XX-XX.tailnet-XXXX.ts.net with TLS options default entryPointName=websecure
起動ログの中にTailscale HTTPSのTLS証明書のメッセージが見えますね。Tailscaleをインストールした手元のMacからアクセスしてみます。
% curl -v https://ip-XX-XX-XX-XX.tailnet-XXXX.ts.net * Trying 100.89.238.14:443... * Connected to ip-XX-XX-XX-XX.tailnet-XXXX.ts.net (100.89.238.14) port 443 (#0) * ALPN: offers h2 * ALPN: offers http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 * ALPN: server accepted h2 * Server certificate: * subject: CN=ip-XX-XX-XX-XX.tailnet-XXXX.ts.net * start date: Jan 29 12:41:45 2023 GMT * expire date: Apr 29 12:41:44 2023 GMT * subjectAltName: host "ip-XX-XX-XX-XX.tailnet-XXXX.ts.net" matched cert's "ip-XX-XX-XX-XX.tailnet-XXXX.ts.net" * issuer: C=US; O=Let's Encrypt; CN=R3 * SSL certificate verify ok. * Using HTTP2, server supports multiplexing * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * h2h3 [:method: GET] * h2h3 [:path: /] * h2h3 [:scheme: https] * h2h3 [:authority: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net] * h2h3 [user-agent: curl/7.86.0] * h2h3 [accept: */*] * Using Stream ID: 1 (easy handle 0x13a815200) > GET / HTTP/2 > Host: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net > user-agent: curl/7.86.0 > accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * Connection state changed (MAX_CONCURRENT_STREAMS == 250)! < HTTP/2 200 < content-type: text/plain; charset=utf-8 < date: Sun, 29 Jan 2023 13:43:59 GMT < content-length: 439 < Hostname: ebdbc05b441b IP: 127.0.0.1 IP: 172.17.0.2 RemoteAddr: 172.17.0.1:55100 GET / HTTP/1.1 Host: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net User-Agent: curl/7.86.0 Accept: */* Accept-Encoding: gzip X-Forwarded-For: 100.113.200.88 X-Forwarded-Host: ip-XX-XX-XX-XX.tailnet-XXXX.ts.net X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: ip-XX-XX-XX-XX.ap-northeast-1.compute.internal X-Real-Ip: 100.113.200.88 * Connection #0 to host ip-XX-XX-XX-XX.tailnet-XXXX.ts.net left intact
アクセス出来ました!TLSネゴシエーションのログからTailscaleノードのDNS名に対応するTLS証明書を受け取っているのが読み取れますね。
まとめ
Tailscale HTTPSのTLS証明書をTraefikから動的に取得する構成をご紹介しました。TraefikはKubernetes IngressコントローラやDockerの連携が充実しているので、VPN越しの遠隔の開発環境のコンテナなどでHTTPSをサポートする手段として便利に使えそうですね。
TraefikにはLet's EncryptのTLS証明書を動的に取得する仕組みが元々含まれていますが、ACME対応のためにインターネット向け構成の手間がかかりがちなところをTailscaleに外出しできるのは地味に便利だと思っています。Cloudflare Tunnelでも、プライベートネットワーク向けにCloudflare SSL/TLSの証明書を提供してくれる仕組みがあると良いかもしれませんね。